package notesDetection.MusicAnalyzer;

import java.util.*;

import notesDetection.MusicAnalyzer.AnalyzeMusicFile.AnalysisPreferences;
import notesDetection.SFT.ArraySFTFunction;
import notesDetection.SFT.Complex;
import notesDetection.SFT.SFTAlgorithm;
import notesDetection.Utilities.GeneralNoteDetection;
import notesDetection.Utilities.NoteInfo;
import notesDetection.Utilities.SampleFunctionCoefficient;
import notesDetection.Utilities.SegmentNotesInfo;
import notesDetection.Utilities.GeneralNoteDetection.Note;

public class FrequencyFinder {

	private double _sampleFrequency;
	private List<Integer> _possibleCycleLengths;
	private int _selectedCycleLength;
	
	private GeneralNoteDetection _generalNotesDetection;
	
	private double _tau;
	private double _delta;
	private double _twoNormLowThreshold;
	
	public FrequencyFinder(double sampleFrequency, double aFrequencyHz, double tau, double delta, double twoNormLowThreshold)
	{
		_sampleFrequency = sampleFrequency;
		_tau = tau;
		_delta = delta;
		_twoNormLowThreshold = twoNormLowThreshold;
		
		//	Initialize dictionary of optional frequencies
//		List<Double> possibleFrequencies = new ArrayList<Double>();
//		
//		for(double frequency = 110.0; frequency < 2200.0; frequency*=Math.pow(2, 1.0/12.0))
//		{
//			possibleFrequencies.add(frequency);
//		}
		
		_possibleCycleLengths = new ArrayList<Integer>();
		
//		for(double frequency : possibleFrequencies)
//		{
//			_possibleCycleLengths.add((int) Math.round(_sampleFrequency / frequency));
//		}
		
		for(int cycleLength=3501; cycleLength <= 4500; cycleLength++)
		{
			_possibleCycleLengths.add(cycleLength);
		}
		
		_generalNotesDetection = new GeneralNoteDetection(aFrequencyHz);
	}
	
	/**
	 * This function should extract the main frequencies in the input segment.
	 * 
	 * Repeatedly:
	 * It calculates the main frequency from the dictionary (by finding the minimal auto-correlation value for the values in the dictionary).
	 * Find the top frequencies of this cycle (with SFT) and add them to the list.
	 * In the next iteration, when "sampling" the function, the previous sines detected will be deducted, so we can find the "new" heavy frequencies.
	 * 
	 * Finally, all detected BASE cycles (up to 3 currently) are returned, which can be converted easily to frequencies == notes
	 *  
	 * INNER PROCEDURE
	 * Stores all the calculated correlation in a dictionary (for DEBUG purposes) and return the length (in samples)
	 * of the segment with minimal auto-correlation value. 
	 * 
	 * @param inputSegment
	 * @param startSample
	 * @param stopSample
	 * @param preferences 
	 * @return
	 */
	public List<Integer> FindSegmentTopCoefficients(double[] inputSegment, int startSample, int stopSample, AnalysisPreferences preferences)
	{
		long correlationMinimumLength = Long.MAX_VALUE;
		double correlationMinimumValue = Double.POSITIVE_INFINITY;

		//ArraySFTFunction functionToAnalyze = new ArraySFTFunction(inputSegment, 1.0);
		
		List<Integer> detectedCycles = new ArrayList<Integer>();
		//	Detect maximum of 3 separate frequencies (DISABLED. use only 1 iteration currently)
		//for(int detectedPeriodStep = 0; detectedPeriodStep < 1; detectedPeriodStep++)
		//{
		TreeMap<Integer, Double> frequencyRanks = new TreeMap<Integer, Double>();
		//	Detect one TOP frequency (reducing frequencies found previously)
		for(int cycleLength : _possibleCycleLengths)
		{
			double currentAutoCorrelation = CalculateAutoCorrelation(inputSegment, cycleLength, startSample, stopSample);
			//	Add to dictionary (helpful for debug).
			frequencyRanks.put(cycleLength, currentAutoCorrelation);
			//	Save minimal correlation value
			if(currentAutoCorrelation < correlationMinimumValue)
			{
				correlationMinimumValue = currentAutoCorrelation;
				correlationMinimumLength = cycleLength;
			}
		}
		
		int newDetectedCycle = (int)correlationMinimumLength;
		detectedCycles.add(newDetectedCycle);
		
		//	for DEBUG:
		//System.out.println("Iteration " + detectedPeriodStep + ": found minimum @ " + newDetectedCycle);
		System.out.println("Found minimum @ " + newDetectedCycle);
		
		//for(int cycleLength : frequencyRanks.keySet())
		//{
		//	System.out.println("Found value: " + 44100.0 / cycleLength + " with rank: " + frequencyRanks.get(cycleLength));
		//}
		
		//	TODO TODO TODO

		//	Find top coefficients (topCycleLength)

		//double TAU = 0.2;
		//double DELTA = 0.5;
		
		double[] segmentForSft = new double[newDetectedCycle];
		
		//	Take sub-segment from center instead of beginning
		int extraOffset = (stopSample - startSample + 1 - newDetectedCycle) / 2;
		
		for(int i=0; i<newDetectedCycle; i++)
		{
			//segmentForSft[i] = functionToAnalyze.funcValue(i).Re();
			segmentForSft[i] = inputSegment[extraOffset + i];
		}
		
		//	Wav file values are in range -1 to 1, therefore INF_NORM == 1
		ArraySFTFunction functionForSft = new ArraySFTFunction(segmentForSft, 1.0);
		
		//	NOTE: Calculate using SFT		
		_selectedCycleLength = newDetectedCycle;
		
		List<Integer> listOfCoeffs = new ArrayList<Integer>();
		
		double currentTau = _tau;	//	Don't change the default TAU for later iterations!
		int attemptsCounter = 9;	//	Limit to number of repetitions (if still not found, give up)
		
		//	Try for several times, lowering the threshold in case no coeffs found
		while(listOfCoeffs.size() == 0 && attemptsCounter-- > 0)
		{
			//	Lower threshold
			currentTau /= 2;
			//	RUN ALGORITHM!!
			listOfCoeffs = SFTAlgorithm.RunSftAlgorithm(functionForSft, currentTau, _delta, _twoNormLowThreshold);
		}
		
		//	SFT Algorithm is finished. Now we have the "top" coefficients.
		return listOfCoeffs;
	}
	
	public List<SegmentNotesInfo> EvaluateCoefficientsValue(List<Integer> listOfCoeffs, double[] inputSegment, int startSample, int stopSample, int segmentId, AnalysisPreferences preferences, int NUMBER_OF_SUB_SEGMENTS)
	{
		
		List<SegmentNotesInfo> outputNotes = new ArrayList<SegmentNotesInfo>();
		//	Prepare output data:
		
		int subSegmentStart;
		int subSegmentStop = -1;
		int windowLength = (int)Math.round(inputSegment.length / (double)NUMBER_OF_SUB_SEGMENTS);
		
		for(int subSegment = 0; subSegment < NUMBER_OF_SUB_SEGMENTS; subSegment++)
		{
			List<NoteInfo> currentNotes = new ArrayList<NoteInfo>();
			
			subSegmentStart = subSegmentStop+1;
			subSegmentStop = subSegment == NUMBER_OF_SUB_SEGMENTS - 1 ? stopSample - startSample : subSegmentStart + windowLength - 1;
			
			
			for(int coeff : listOfCoeffs)
			{
				if(coeff == -1)
				{
					//	That's a quiet segment (twoNorm was below threshold)
					currentNotes.add(new NoteInfo(Double.NaN, Note.QuietSegment, Double.NaN, Double.NaN));
					break;
				}

				Complex sampledCoeffValue = SampleFunctionCoefficient.SampleCoeffValueFull(inputSegment, coeff, _selectedCycleLength, subSegmentStart, subSegmentStop);
				

					
			    //if (entry != null && _sampleFrequency / _selectedCycleLength * entry.getKey() < 20000)
				if (_sampleFrequency / _selectedCycleLength * coeff < 20000)
			    {
			    	double currentFrequency = _sampleFrequency / _selectedCycleLength * coeff;
			    	
			    	double currentCoeffAbs = sampledCoeffValue.Abs();
			    	double currentPhase = sampledCoeffValue.Phase();
			    	
			    	//if(currentCoeffAbs > maxValue)
			    	//{
			    	//	maxEntry = entry;
			    	//	maxValue = currentCoeffAbs;
			    	//}
			    	
//				    	List<SineInfo> segmentSineList = new ArrayList<SineInfo>();
			    	
			    	//segmentSineList.add(new SineInfo(currentCoeffAbs, currentPhase, 10));
			    	
			    	NoteInfo currentNoteInfo = new NoteInfo(currentFrequency, _generalNotesDetection.GetFrequencyAsNote(currentFrequency), currentCoeffAbs, currentPhase);
//				    	Print max only...
			    	//if(!printMaxOnly)
			    	//{
			    	System.out.println("Max found: " + _generalNotesDetection.GetFrequencyAsNote(currentFrequency).toString() + " " + currentFrequency + " value: " + currentCoeffAbs);
			    	//}
			    	
			    	currentNotes.add(currentNoteInfo);
			    }
				
			}
			
			outputNotes.add(new SegmentNotesInfo(startSample + subSegmentStart, startSample + subSegmentStop, segmentId, currentNotes));

			
			int numberOfCoeffsFound = listOfCoeffs.size();
			
			if(numberOfCoeffsFound > 20)
			{
				//	That's usually noise or a non-cyclic part...
				System.out.println("Too many coeffs, weak cycle");
			}
			
			System.out.println("### Number of coefficients: "+ numberOfCoeffsFound +" ###");
			
			//boolean printMaxOnly = false;
			
			//Map.Entry<Integer, Complex> maxEntry = null;
			//double maxValue = Double.NEGATIVE_INFINITY;
			


//			if(printMaxOnly)
//			{
//				double topFrequency = _sampleFrequency / _selectedCycleLength * maxEntry.getKey();;
//		    	System.out.println("Max found: " + GeneralNoteDetection.GetFrequencyAsNote(topFrequency).toString() + " " + topFrequency + " value: " + maxEntry.getValue());
//			}
						
			//	Create collection of (phase + amplitude) of each frequency, and reduce all frequencies from the sample at next phase
			
			//	NOTE: Calculate using FFT
			//Complex[] fftListOfCoeffs = FTLibrary.ArrayOfAllCharacters(functionForSft);
			
			//TreeMap<Integer, Double> absValue = new TreeMap<Integer, Double>();
			
			//	Don't add DC coefficient!
//			for(int coeffIndex = 1; coeffIndex < newDetectedCycle / 2; coeffIndex++)
//			{
//				double realValue = fftListOfCoeffs[coeffIndex].Re();
//				double imagValue = fftListOfCoeffs[coeffIndex].Im();
//				absValue.put(coeffIndex, realValue*realValue + imagValue * imagValue);
//				detectedHarmonics.add(new Harmonic(newDetectedCycle * coeffIndex, realValue, imagValue));
//			}
			

//			for(int getMaxVals = 0; getMaxVals < 4; getMaxVals++)
//			{
//				Map.Entry<Integer, Double> maxEntry = null;
	//
//				for (Map.Entry<Integer, Double> entry : absValue.entrySet())
//				{
//				    if (maxEntry == null || entry.getValue().compareTo(maxEntry.getValue()) > 0)
//				    {
//				        maxEntry = entry;
//				    }
//				}
//				
//				System.out.println("Max found: " + _sampleFrequency / newDetectedCycle * maxEntry.getKey() + " value: " + maxEntry.getValue());
//				absValue.remove(maxEntry.getKey());
//			}

			//}
			
		}
		return outputNotes;

	}

	/**
	 * 
	 * @param inputSegment
	 * @param cycleLength
	 * @return
	 */
	private double CalculateAutoCorrelation(double[] inputSegment,
			int cycleLength, int startSample, int stopSample)
	{
		final int NUMBER_OF_ATTEMPTS = 2000;
		
		//	Else it is useless to calculate correlation. segment is too short
		//	return Double.NaN? maybe...
		assert 2*cycleLength <= stopSample - startSample;
		
	
		double totalCorrelation = 0.0;
		for(int attemptNumber = 0; attemptNumber < NUMBER_OF_ATTEMPTS; attemptNumber++)
		{
			int correlationStart = GeneralNoteDetection.GetRandomInt(startSample, stopSample - cycleLength);
			
			double firstValue = inputSegment[correlationStart];
			double secondValue = inputSegment[correlationStart + cycleLength];
			//double firstValue = SubtractDetectedHarmonies(inputSegment[correlationStart], correlationStart, detectedHarmonics);
			//double secondValue = SubtractDetectedHarmonies(inputSegment[correlationStart + cycleLength], correlationStart, detectedHarmonics);
			
			totalCorrelation += Math.pow(firstValue - secondValue, 2);
		}

		return totalCorrelation / NUMBER_OF_ATTEMPTS;
	}

	/**
	private double SubtractDetectedHarmonies(double sampleValue, int sampleIndex, List<Harmonic> detectedHarmonics)
	{
		for(Harmonic harmonic : detectedHarmonics)
		{
			sampleValue -= harmonic.getValueAt(sampleIndex);
		}
		return sampleValue;
	}
	 */

}
